Skip to content

Conversation

@jkczyz
Copy link
Contributor

@jkczyz jkczyz commented Dec 18, 2025

Rather than requiring the user to pass FundingTxInputs when initiating a splice, generate a FundingNeeded event once the channel has become quiescent. This simplifies error handling and UTXO / change address clean-up by consolidating it in SpliceFailed event handling.

Later, this event will be used for opportunistic contributions (i.e., when the counterparty wins quiescence or initiates), dual-funding, and RBF.

Based on #4261.

This is still fairly rough. It does not yet include any code for creating a FundingNegotiationContext from a FundingContribution. The former may need to a dedicated struct instead so that any data needed from ChannelManager or ChannelContext can be produced internally. Alternatively, that data could be included in FundingContribution, but it would need to be serializable.

@ldk-reviews-bot
Copy link

ldk-reviews-bot commented Dec 18, 2025

👋 Thanks for assigning @wpaulino as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@jkczyz jkczyz self-assigned this Dec 18, 2025
@jkczyz jkczyz force-pushed the 2025-12-new-splice-api branch from f5933e5 to 854e9ca Compare January 12, 2026 17:47
@jkczyz
Copy link
Contributor Author

jkczyz commented Jan 12, 2026

@TheBlueMatt @wpaulino Looking for some high-level feedback on the API introduced in the last commit. In summary:

  • User passes SpliceContribution -- which no longer contains any FundingTxInputs -- to ChannelManager::splice_channel
  • Upon quiescence LDK generates a FundingNeeded event which contains a FundingTemplate
  • User calls FundingTemplate::build or FundingTemplate::build_sync with a WalletSource or WalletSourceSync, respectively, to produce a FundingContribution
  • User passes FundingContribution -- which contains the FundingTxInputs -- to ChannelManager::funding_contributed
  • LDK validates that the FundingContribution can pay for inputs / outputs, causing LDK to either send splice_init or produce a SpliceFailed event.

The same mechanism can be used later for contributing inputs for counterparty-initiated splices or v2 channel opens since FundingTemplate and FundingContribution contains the context.

Test code still needs to be fixed up, and change_script generation will follow in another commit.

Copy link
Contributor

@wpaulino wpaulino left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API design LGTM, though there's one issue with WalletSource. One thing users need to keep in mind now is that from the moment they receive FundingNeeded, they need to act quickly to ensure the counterparty doesn't disconnect due to quiescence taking too long.

fn list_confirmed_utxos(&self) -> Result<Vec<Utxo>, ()>;

///
fn select_confirmed_utxos(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding this here now requires implementers to satisfy this method when, in the context of anchor channels, WalletSource is only intended to be used such that we perform coin selection on behalf of the user. Ideally, we also give users the option between choosing WalletSource/CoinSelectionSource when funding channels.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I guess I'm a bit confused why we can't use select_confirmed_utxos as-is? Indeed the claim_id is annoying, but we can make that either an enum across a ClaimId and some unit value describing a splice or just make it an Option. Aside from that it seems to be basically what we want.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding this here now requires implementers to satisfy this method when, in the context of anchor channels, WalletSource is only intended to be used such that we perform coin selection on behalf of the user. Ideally, we also give users the option between choosing WalletSource/CoinSelectionSource when funding channels.

Hmm... I see. Would a separate trait be desirable? Also, see my reply to @TheBlueMatt below.

Right, I guess I'm a bit confused why we can't use select_confirmed_utxos as-is? Indeed the claim_id is annoying, but we can make that either an enum across a ClaimId and some unit value describing a splice or just make it an Option. Aside from that it seems to be basically what we want.

The return value also isn't compatible. It contains Utxos but we also need the previous tx and sequence number as part of each FundingTxInput. Though its constructor will give a default sequence number.

We could change CoinSelection to use FundingTxInput instead of Utxo, but that would be odd for use with the anchor context.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly that seems fine to me? We expect ~all of our users to want to use splicing, which implies they need to support the "return coin selection with full transactions" interface. So what if anchors throw away some of that data?

If we feel strongly about it we can add a new trait method that does return the full transactions and provide a default implementation for the current method so that those that really want to avoid always fetching the transaction data can.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I don't have a strong opinion, but note that Wallet's implementation of CoinSelectionSource::select_confirmed_utxos delegates to WalletSource::list_confirmed_utxos. So it might be expensive to use that abstraction. @wpaulino WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking of keeping CoinSelectionSource the same (with the full transaction data in the response, or with the default-impl-method described above) but changing WalletSource so that we don't have to fetch all previous-transactions at the start.

Yeah, Wallet wraps a WalletSource and implements CoinSelectionSource by listing all the UTXOs and selecting from them. So WalletSource's interface would remain unchanged while CoinSelection would use FundingTxInput instead of Utxo. Which I guess means FundingTemplate::build should actually take a CoinSelectionSource.

Seems reasonable to require a sequence number in the response for that as well, even for anchors?

Hmm... in WalletSource::list_confirmed_utxos by adding a field to Utxto (and removing it from FundingTxInput)? Or by having the CoinSelectionSource implementation fill it in on FundingTxInput?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So WalletSource's interface would remain unchanged

Wouldn't we need a WalletSource::get_previous_transaction_for_utxo method to fetch the full tx data for the UTXOs we selected?

Hmm... in WalletSource::list_confirmed_utxos by adding a field to Utxto (and removing it from FundingTxInput)? Or by having the CoinSelectionSource implementation fill it in on FundingTxInput?

ISTM we should replace Utxo with FundingTxInput since FundingTxInput has strictly more fields (it contains a Utxo!) and we'd move to returning FundingTxInput from the trait.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't we need a WalletSource::get_previous_transaction_for_utxo method to fetch the full tx data for the UTXOs we selected?

Right, we need another method for that.

ISTM we should replace Utxo with FundingTxInput since FundingTxInput has strictly more fields (it contains a Utxo!) and we'd move to returning FundingTxInput from the trait.

The question is more what should be setting Sequence? Either:

(1) Move it to Utxo and have WalletSource::list_confirmed_utxos set it since it returns Vec<Utxo>.
(2) Have CoinSelectionSource::select_confirmed_utxos set it since CoinSelection would now contain Vec<FundingTxInput>

We just can't replace Utxo with FundingTxInput in WalletSource::list_confirmed_utxos since we don't want to return the previous tx there.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(1) Move it to Utxo and have WalletSource::list_confirmed_utxos set it since it returns Vec.

Presumably this. No reason to want it to not be possible in WalletSource.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done as discussed here and offline. I'm in the middle of updating the tests, but I've pushed an update for now.

Copy link
Collaborator

@TheBlueMatt TheBlueMatt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aside from the above which-interface question I think the API is good.

@jkczyz jkczyz force-pushed the 2025-12-new-splice-api branch from 854e9ca to 6d78c3f Compare January 14, 2026 17:03
@ldk-reviews-bot
Copy link

🔔 1st Reminder

Hey @TheBlueMatt @wpaulino! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

1 similar comment
@ldk-reviews-bot
Copy link

🔔 1st Reminder

Hey @TheBlueMatt @wpaulino! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@jkczyz jkczyz force-pushed the 2025-12-new-splice-api branch from 6d78c3f to 94b1aa9 Compare January 15, 2026 17:02
@jkczyz jkczyz mentioned this pull request Jan 16, 2026
@ldk-reviews-bot
Copy link

🔔 2nd Reminder

Hey @TheBlueMatt @wpaulino! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

1 similar comment
@ldk-reviews-bot
Copy link

🔔 2nd Reminder

Hey @TheBlueMatt @wpaulino! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot
Copy link

🔔 3rd Reminder

Hey @TheBlueMatt @wpaulino! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

1 similar comment
@ldk-reviews-bot
Copy link

🔔 3rd Reminder

Hey @TheBlueMatt @wpaulino! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@jkczyz jkczyz force-pushed the 2025-12-new-splice-api branch from 94b1aa9 to c3f3453 Compare January 20, 2026 19:16
Comment on lines 190 to 165
// FIXME: Should claim_id be an Option?
let claim_id = ClaimId([0; 32]);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding the CoinSelectionSource API, do we want to make claim_id an Option?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO yes

Comment on lines 883 to 884
Amount::from_sat(383)
Amount::from_sat(385)
} else {
Amount::from_sat(384)
Amount::from_sat(386)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems select_confirmed_utxos_internal might be off on the change calculation because it's using the weight of the change output to compute additional fees instead of re-computing the total fees using the total weight when including a change output.

@jkczyz jkczyz marked this pull request as ready for review January 20, 2026 19:25
@jkczyz jkczyz force-pushed the 2025-12-new-splice-api branch from c3f3453 to 3253a99 Compare January 20, 2026 23:57
@ldk-reviews-bot
Copy link

🔔 4th Reminder

Hey @TheBlueMatt @wpaulino! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

1 similar comment
@ldk-reviews-bot
Copy link

🔔 4th Reminder

Hey @TheBlueMatt @wpaulino! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@jkczyz jkczyz force-pushed the 2025-12-new-splice-api branch 3 times, most recently from f2d9fa5 to fcccbac Compare January 21, 2026 18:22
@codecov
Copy link

codecov bot commented Jan 21, 2026

Codecov Report

❌ Patch coverage is 77.17087% with 163 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.91%. Comparing base (8eb9e70) to head (12eee3d).
⚠️ Report is 32 commits behind head on main.

Files with missing lines Patch % Lines
lightning/src/ln/channel.rs 50.23% 101 Missing and 4 partials ⚠️
lightning/src/ln/funding.rs 90.73% 12 Missing and 17 partials ⚠️
lightning/src/ln/channelmanager.rs 77.64% 19 Missing ⚠️
lightning/src/events/bump_transaction/mod.rs 90.47% 3 Missing and 1 partial ⚠️
lightning/src/ln/functional_test_utils.rs 40.00% 3 Missing ⚠️
lightning/src/events/mod.rs 87.50% 0 Missing and 2 partials ⚠️
lightning/src/util/ser.rs 85.71% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4290      +/-   ##
==========================================
- Coverage   86.61%   85.91%   -0.71%     
==========================================
  Files         158      156       -2     
  Lines      103067   102751     -316     
  Branches   103067   102751     -316     
==========================================
- Hits        89275    88282     -993     
- Misses      11366    11969     +603     
- Partials     2426     2500      +74     
Flag Coverage Δ
fuzzing ?
tests 85.91% <77.17%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Collaborator

@TheBlueMatt TheBlueMatt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

API looks good. some misc comments i noted while skimming it

}

/// An unspent transaction output with at least one confirmation.
pub type ConfirmedUtxo = FundingTxInput;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets not rename types with aliases. If we are making a type more general lets rename the type itself.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, though it seems like neither funding nor bump_transaction are suitable places for this or the CoinSelection / Wallet related traits and types now. Should we move these into a separate module under util or elsewhere?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to wallet_utils courtesy of claude. I reversed the type alias for now. I could drop it if you prefer. Just seems like FundingTxInput reads a little better in funding contexts.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropped this for a follow-up PR.

/// Indicates that funding is needed for a channel splice or a dual-funded channel open.
///
/// The client should build a [`FundingContribution`] from the provided [`FundingTemplate`] and
/// pass it to [`ChannelManager::funding_contributed`].
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs a call-out to what to do if you actually don't want to splice anymore (ie on failure)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also maybe a note that the channel is hung waiting on our response, so we need to respond quickly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should they simply not handle the event? We'll be in quiescence and timeout after DISCONNECT_PEER_AWAITING_RESPONSE_TICKS. Though it seems this now inadvertently (but maybe expectedly) now applies to us sending splice_init. So maybe some renaming is in order?

Or should we expose ChannelManager::exit_quiescence?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that the presence of this event in the pending events queue determines whether or not a splice can be initiated, no action is needed from the user if they no longer want to splice.

Comment on lines 190 to 165
// FIXME: Should claim_id be an Option?
let claim_id = ClaimId([0; 32]);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO yes


/// Creates a `FundingContribution` from the template by using `wallet` to perform coin
/// selection with the given fee rate.
pub fn build_sync<W: Deref>(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also have a method for building with a provided set of inputs rather than going through the trait?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could... or probably a CoinSelection so that a change output can be given.

if self.context.channel_state.is_quiescent() {
return Err(APIError::APIMisuseError {
err: format!(
"Channel {} cannot be spliced as it is already quiescent",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we want to support queuing up the splice to do afterwards?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm... this was to handle the time after we've generated FundingNeeded when we no longer have a quiescent_action but are still quiescent waiting on the user to call funding_contributed. But then we can't differentiate this from counterparty-initiated quiescence. Maybe we need to make a placeholder QuiescentAction for when we are waiting on the user to respond? Something like AwaitingFundingContribution, which could also be checked when funding_contributed is called.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding error handling, since the FundingContribution is taken by value we need to generate a SpliceFailed event to allow the user to unlock the UTXOs. However, some failure may be result of misuse (e.g., wrong channel / counterparty, unexpected funding) or bad timing (e.g., peer already disconnected timing out quiescence).

In those cases, it doesn't make sense to generate SpliceFailed. Maybe we need to use DiscardFunding for some of these cases? We'd need another FundingInfo variant, though.

Otherwise, we'd need to return the UTXOs back to the caller, which we wanted to avoid.

Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll leave this for a follow-up PR.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the motivation for this is no longer relevant now that FundingNeeded comes before quiescence. We should definitely allow queueing the splice when quiescent, but only if we're not the initiator and/or we're attempting a different quiescent protocol (not possible yet). If we're already splicing, we should suggest doing an RBF once that's supported.

) -> Result<msgs::SpliceInit, SpliceFundingFailed>
where
L::Target: Logger,
{
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

presumably we need to check that we're quiescent and its our turn to talk?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on offline discussion, we are no longer waiting to enter quiescence to generate FundingNeeded. It will instead be initiated when funding_contributed is called.

impl_writeable_tlv_based_enum_upgradable!(QuiescentAction,
(0, DoNothing) => {},
{1, Splice} => (),
{1, LegacySplice} => (),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs docs on when we switched so we know when to remove it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this still needs to be addressed but on the non-test/fuzzing version below

@jkczyz
Copy link
Contributor Author

jkczyz commented Jan 22, 2026

Given that the new API is to be reused for RBF, I wrote up a proposal to consider with this PR. @TheBlueMatt @wpaulino Looking for feedback / alternative ideas before continuing that work.

Observations

  • DiscardFunding contains the funding outpoint. So I assume users will need to look up the transaction to unlock any UTXOs and reclaim script pubkeys?
  • For RBF, I see we have FundingScope::funding_transaction, which is an Option. I thought we wanted to clear this, but I don't see where we are doing that.
  • Either of those require the user to know which outputs are their own and isn't change in order to recreate the SpliceContribution.
  • We produce a FundingTemplate from a SpliceContribution. The user may add more value / outputs before constructing a FundingContribution.
  • When implementing the acceptor case, there will not be a  SpliceContribution -- just an empty FundingTemplate to be modified if needed before producing a FundingContribution. Similarly for dual funding.
  • FundingContribution can't differentiate user selected UTXOs from change.

Proposal

  • Store change output separately in FundingContribution instead of losing the information to outputs.
  • Store the shared input in FundingContribution to allow recreating the FundingTemplate
  • Recreate the FundingTemplate internally from the FundingContribution (before it is consumed into a FundingNegotiationContext) and keep track of it until it can be included alongside the negotiated FundingScopes
  • Alternatively, re-create FundingTemplate somehow from the InteractiveTxConstructor / InteractiveTxSigningSession data. Note, however, that which output, if any, is change will have been already lost.
  • Expose these FundingTemplates in ChannelDetails for informational purposes.
  • User initiated RBF doesn't require passing a SpliceContribution.
  • Instead another FundingNeeded event is generated -- once the channel is quiescent -- containing the last used FundingTemplate
  • User uses any of the previously used FundingTemplates or the one given in the event (modifying as they see fit) to call ChannelManager::funding_contributed.

Questions

  • How should we expose the FeeRate used in each negotiation? We need to make sure the RBF uses a greater fee rate, so at very least we need to keep track of it. User also needs to know to pass a higher fee rate when creating the new FundingContribution.
  • Do we need to also expose the inputs used in each negotiation? They aren't needed for RBF, so would only be informational.

@ldk-reviews-bot
Copy link

🔔 5th Reminder

Hey @TheBlueMatt @wpaulino! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot
Copy link

🔔 1st Reminder

Hey @TheBlueMatt @wpaulino! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot
Copy link

🔔 6th Reminder

Hey @TheBlueMatt @wpaulino! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@TheBlueMatt
Copy link
Collaborator

We discussed this a bunch offline on friday and I believe there are some further changes here. Let us know when you're ready for another look.

@jkczyz jkczyz force-pushed the 2025-12-new-splice-api branch from fcccbac to 12eee3d Compare January 27, 2026 18:57
@jkczyz
Copy link
Contributor Author

jkczyz commented Jan 27, 2026

We discussed this a bunch offline on friday and I believe there are some further changes here. Let us know when you're ready for another look.

Pushed changes to move FundingNeeded prior to quiescence. This involved having QuiescentAction::AwaitingFunding contain a FundingContribution and LockTime. This is set but without set_awaiting_quiescence, which needs to be scrutinized. Is it possible to get stuck in this state?

Additionally, updated splice_channel to take a FeeRate instead of funding_contributed. This will help with RBF as the corresponding FundingNeeded event can be generated with an appropriate fee rate. Even if provided by user when initiating an RBF, it can at least be checked.

Working on separating contributed inputs and outputs from SpliceFailed into a separate DiscardFunding event since there are instances where we need this when splice_channel (and maybe funding_contributed?) fails.

.map_err(|e| APIError::APIMisuseError { err: e.to_owned() })
// Don't use propose_quiescence as we don't want to initiate quiescence until FundingNeeded
// has been processed. This prevents initiating other quiescent actions in the meanwhile.
self.quiescent_action = Some(QuiescentAction::AwaitingFunding);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This changes the semantics of QuiescentAction from an action to take after becoming quiescent to also include actions before becoming quiescent. Can we instead rely on a FundingNeeded event still needing to be handled to fail early here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that was essentially what this was standing in for. Alternatively, IIUC, we'd need some other state to track that, right? Something like adding another flag to ChannelReadyFlags?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed offline. We'll eventually want to queue up QuiescentActions, but it might be fine to let a second action jump ahead if the user hasn't processed FundingNeeded. The recent push checks if the FundingNeeded event is present to determine if a second splice attempt should be rejected. PTAL.

@jkczyz jkczyz requested a review from wpaulino January 27, 2026 19:58
@jkczyz jkczyz force-pushed the 2025-12-new-splice-api branch 2 times, most recently from 2e44da8 to 1c8e7e9 Compare January 28, 2026 19:13
@jkczyz
Copy link
Contributor Author

jkczyz commented Jan 28, 2026

Squashed

@jkczyz jkczyz force-pushed the 2025-12-new-splice-api branch from 1c8e7e9 to 78b4f3c Compare January 29, 2026 01:09
Copy link
Contributor Author

@jkczyz jkczyz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can split out the wallet_utils refactor into a separate PR, if preferred.

/// Indicates that funding is needed for a channel splice or a dual-funded channel open.
///
/// The client should build a [`FundingContribution`] from the provided [`FundingTemplate`] and
/// pass it to [`ChannelManager::funding_contributed`].
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that the presence of this event in the pending events queue determines whether or not a splice can be initiated, no action is needed from the user if they no longer want to splice.

) -> Result<msgs::SpliceInit, SpliceFundingFailed>
where
L::Target: Logger,
{
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on offline discussion, we are no longer waiting to enter quiescence to generate FundingNeeded. It will instead be initiated when funding_contributed is called.

}

/// An unspent transaction output with at least one confirmation.
pub type ConfirmedUtxo = FundingTxInput;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to wallet_utils courtesy of claude. I reversed the type alias for now. I could drop it if you prefer. Just seems like FundingTxInput reads a little better in funding contexts.

if self.context.channel_state.is_quiescent() {
return Err(APIError::APIMisuseError {
err: format!(
"Channel {} cannot be spliced as it is already quiescent",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll leave this for a follow-up PR.

@ldk-reviews-bot
Copy link

🔔 1st Reminder

Hey @TheBlueMatt @wpaulino! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

}
}
Ok(tx)
// FIXME: Why does handle_channel_close override the witness?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure i understand this comment

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, this is related to two commits later where test_op_return_under_funds is updated to not use a hardcoded value for the outpoint's txid. But then I realized the calling code handle_channel_close overrides the witness.

https://github.com/jkczyz/rust-lightning/blob/f3eb94ea54368a2cd1fa1eec5a0743451951dda1/lightning/src/events/bump_transaction/mod.rs#L921

So my question is why are we setting the witnesses in TestCoinSelectionSource::sign_pstb if not necessary for the test? Maybe that test should be checking its TestBroadcaster?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sign_psbt is meant to only sign the inputs originating from the wallet

// TODO: Do we care about cleaning this up once the UTXOs have a confirmed spend? We can do so
// by checking whether any UTXOs that exist in the map are no longer returned in
// `list_confirmed_utxos`.
locked_utxos: Mutex<HashMap<OutPoint, Option<ClaimId>>>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to handle DiscardFunding now so that we can drop entries if a splice RBFs?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As in add a method on Wallet? Yeah, though I'll likely do that in a follow-up when refactoring DiscardFunding from SpliceFailed events.

For RBF, did we want to produce DiscardFunding before FundingNeeded so that the UTXOs can be reused in the RBF? Though an earlier splice / RBF attempt may still get confirmed...

IIUC, we already have DiscardFunding events generated by the ChannelMonitor once the splice is promoted. But ISTM that wouldn't let re-use the UTXOs in the RBF tx unless we generalize ClaimId to be used for splicing / dual-funding? Though, at least in the Wallet implementation`, we'll fall back to reusing UTXOs if we don't have any others to use.

@ldk-reviews-bot
Copy link

🔔 1st Reminder

Hey @wpaulino! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@jkczyz jkczyz force-pushed the 2025-12-new-splice-api branch from 78b4f3c to cbf324b Compare January 30, 2026 17:05
A forthcoming commit will change CoinSelection to include FundingTxInput
instead of Utxo, though the former will probably be renamed. This is so
CoinSelectionSource can be used when funding a splice. Further updating
WalletSource to use FundingTxInput is not desirable, however, as it
would result in looking up each confirmed UTXOs previous transaction
even if it is not selected. See Wallet's implementation of
CoinSelectionSource, which delegates to WalletSource for listing all
confirmed UTXOs.

This commit moves FundingTxInput::sequence to Utxo, and thus the
responsibility for setting it to WalletSource implementations. Doing so
will allow Wallet's CoinSelectionSource implementation to delegate
looking up previous transactions to WalletSource without having to
explicitly set the sequence on any FundingTxInput.
In order to reuse CoinSelectionSource for splicing, the previous
transaction of each UTXO is needed. Update CoinSelection to use
FundingTxInput (renamed to ConfirmedUtxo) so that it is available.

This requires adding a method to WalletSource to look up a previous
transaction for a UTXO. Otherwise, Wallet's implementation of
CoinSelectionSource would need WalletSource to include the previous
transactions when listing confirmed UTXOs to select from. But this would
be inefficient since only some UTXOs are selected.
CoinSelectionSource is used for anchor bumping where a ClaimId is passed
in to avoid double spending other claims. To re-use this trait for
funding a splice, the ClaimId must be optional. And, if None, then any
locked UTXOs may be considered ineligible by an implementation.
Rather than requiring the user to pass FundingTxInputs when initiating a
splice, generate a FundingNeeded event once the channel has become
quiescent. This simplifies error handling and UTXO / change address
clean-up by consolidating it in SpliceFailed event handling.

Later, this event will be used for opportunistic contributions (i.e.,
when the counterparty wins quiescence or initiates), dual-funding, and
RBF.
Now that CoinSelection is used to fund a splice funding transaction, use
that for determining of a change output should be used. Previously, the
initiator could either provide a change script upfront or let LDK
generate one using SignerProvider::get_destination_script.

Since older versions may have serialized a SpliceInstruction without a
change script while waiting on quiescence, LDK must still generate a
change output in this case.
@jkczyz jkczyz force-pushed the 2025-12-new-splice-api branch from cbf324b to 42d0251 Compare January 30, 2026 17:48
@jkczyz
Copy link
Contributor Author

jkczyz commented Jan 30, 2026

Squashed and rebased

}
}
Ok(tx)
// FIXME: Why does handle_channel_close override the witness?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sign_psbt is meant to only sign the inputs originating from the wallet

let mut confirmed_utxos = Vec::with_capacity(selected_utxos.len());
for (utxo, _) in selected_utxos {
let prevtx = self.source.get_prevtx(&utxo).await?;
confirmed_utxos.push(ConfirmedUtxo { utxo, prevtx });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth sanity checking the transaction actually matches the utxo?

/// [`ChannelMonitor::rebroadcast_pending_claims`]: crate::chain::channelmonitor::ChannelMonitor::rebroadcast_pending_claims
fn select_confirmed_utxos<'a>(
&'a self, claim_id: ClaimId, must_spend: Vec<Input>, must_pay_to: &'a [TxOut],
&'a self, claim_id: Option<ClaimId>, must_spend: Vec<Input>, must_pay_to: &'a [TxOut],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a fan that we're complicating this already complex argument even more, but this really needs docs explaining the desired behavior when None

(13, *contributed_outputs, optional_vec),
});
},
&Event::FundingNeeded {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need to persist this? Seems like we should only be persisting our intent to splice after we get a contribution back.

Comment on lines +4707 to +4657
if self
.pending_events
.lock()
.unwrap()
.iter()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An intermediate variable would help here


let (our_funding_inputs, our_funding_outputs, change_script) = contribution.into_tx_parts();
// At this point forward any failure should result in SpliceFundingFailed
self.quiescent_action = None;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this already be None?

debug_assert!(contribution.is_splice());

if self.context.channel_state.is_awaiting_quiescence()
|| self.context.channel_state.is_quiescent()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want to support queuing our own splice while the counterparty is doing one, then we should reconsider this.

),
});
}
self.validate_splice_contributions(our_funding_contribution, their_funding_contribution)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this here when we're already checking it in funding_contributed? Do we really need the value_added upfront instead of getting it in funding_contributed?

self.propose_quiescence(logger, QuiescentAction::Splice { contribution, locktime }).map_err(
|(e, action)| {
log_error!(logger, "{}", e);
// FIXME: Any better way to do this?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do what exactly?

Some(QuiescentAction::Splice { contribution, locktime }) => {
if self.pending_splice.is_some() {
self.quiescent_action =
Some(QuiescentAction::Splice { contribution, locktime });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a TODO somewhere here that we can RBF while the splice is pending?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

4 participants